Skip to content

Wire MCPAuthzConfig references into workload controllers#4778

Closed
JAORMX wants to merge 2 commits into
mainfrom
jaosorior/mcpauthzconfig-wiring
Closed

Wire MCPAuthzConfig references into workload controllers#4778
JAORMX wants to merge 2 commits into
mainfrom
jaosorior/mcpauthzconfig-wiring

Conversation

@JAORMX

@JAORMX JAORMX commented Apr 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • The MCPAuthzConfig CRD from Add MCPAuthzConfig CRD for backend-agnostic authorization #4777 defines shared authorization configuration, but workload controllers don't yet consume it. This wires the references so that MCPServer, MCPRemoteProxy, and VirtualMCPServer can resolve and apply authorization from referenced MCPAuthzConfig resources.
  • Adds CEL mutual exclusion validation, hash-based change detection, status conditions, controller watches, volume mount generation, and runtime config resolution for all three workload types.

Type of change

  • New feature

Test plan

  • Unit tests (task test)
  • Linting (task lint-fix)

Changes

File Change
api/v1alpha1/mcpserver_types.go Add AuthzConfigRef condition constants, AuthzConfigHash status field, CEL mutual exclusion
api/v1alpha1/mcpremoteproxy_types.go Add AuthzConfigHash status field, CEL mutual exclusion
api/v1alpha1/virtualmcpserver_types.go Add AuthzConfigHash status field, CEL mutual exclusion on IncomingAuthConfig
pkg/controllerutil/authz.go Add GetAuthzConfigForWorkload, ValidateAuthzConfigReady, GenerateAuthzVolumeConfigFromRef, EnsureAuthzConfigMapFromRef, BuildFullAuthzConfigJSON (moved from controller), AddAuthzConfigRefOptions
controllers/mcpauthzconfig_controller.go Use shared BuildFullAuthzConfigJSON from controllerutil
controllers/mcpserver_controller.go Add handleAuthzConfig, setAuthzConfigRefCondition, mapAuthzConfigToMCPServer watch, AuthzConfigRef volume mount
controllers/mcpserver_runconfig.go Resolve AuthzConfigRef in runconfig builder
controllers/mcpremoteproxy_controller.go Add handleAuthzConfig, mapAuthzConfigToMCPRemoteProxy watch
controllers/mcpremoteproxy_runconfig.go Resolve AuthzConfigRef in runconfig builder, extract addAuthzOptions helper
controllers/virtualmcpserver_controller.go Add handleAuthzConfig, validateConfigRefs, mapAuthzConfigToVirtualMCPServer watch
pkg/virtualmcpserverstatus/types.go Add SetAuthzConfigHash to StatusManager interface
pkg/virtualmcpserverstatus/collector.go Implement SetAuthzConfigHash
pkg/vmcpconfig/converter.go Add resolveAuthzConfigRef for Cedar policy extraction from MCPAuthzConfig

Special notes for reviewers

  • Follows the existing OIDC config ref pattern exactly (fetch → validate ready → track hash → set condition → watch).
  • BuildFullAuthzConfigJSON was moved from the MCPAuthzConfig controller to controllerutil so both the config controller and workload controllers can use it without circular imports.
  • The vmcpconfig converter currently only extracts Cedar policies from MCPAuthzConfig. Full support for non-Cedar backends in VirtualMCPServer runtime config is tracked as a follow-up (vmcpconfig.AuthzConfig is Cedar-specific).

Generated with Claude Code

JAORMX and others added 2 commits April 13, 2026 13:32
The existing AuthzConfigRef inline variant is Cedar-specific, with
fields that only make sense for the Cedar authorizer. This adds a new
MCPAuthzConfig CRD that accepts any registered authorizer backend
configuration via a type discriminator and an opaque config blob.

The MCPAuthzConfig CRD follows the established shared-config pattern
(MCPOIDCConfig, MCPExternalAuthConfig) with status conditions, hash-based
change detection, referencing workload tracking, and deletion protection.

A new ConfigKey() method on the AuthorizerFactory interface enables the
controller to reconstruct the full runtime config JSON from the CRD spec.

The AuthzConfigRef field is added to MCPServer, MCPRemoteProxy, and
VirtualMCPServer as a new option alongside the existing authzConfig field,
enabling incremental migration with no breaking changes.

Refs: #3157

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
MCPAuthzConfig resources created in PR 1 need to be consumed by the
workload controllers (MCPServer, MCPRemoteProxy, VirtualMCPServer) so
that shared authorization configs actually take effect at runtime.

- Add CEL mutual exclusion for authzConfig/authzConfigRef on all specs
- Add AuthzConfigHash to all workload status types for change detection
- Add condition constants (AuthzConfigRefValidated) and reasons
- Move BuildFullAuthzConfigJSON to controllerutil for shared use
- Add controllerutil helpers: GetAuthzConfigForWorkload,
  ValidateAuthzConfigReady, GenerateAuthzVolumeConfigFromRef,
  EnsureAuthzConfigMapFromRef, AddAuthzConfigRefOptions
- Add handleAuthzConfig to MCPServer, MCPRemoteProxy, VirtualMCPServer
  controllers following the OIDC config ref pattern
- Add MCPAuthzConfig watches to all three workload controllers
- Update runconfig builders to resolve AuthzConfigRef
- Update vmcpconfig converter to extract Cedar policies from
  MCPAuthzConfig for VirtualMCPServer runtime config
- Add SetAuthzConfigHash to VirtualMCPServer status manager

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the size/L Large PR: 600-999 lines changed label Apr 13, 2026
@codecov

codecov Bot commented Apr 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 9.60000% with 339 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.44%. Comparing base (734e8db) to head (440ffe3).

Files with missing lines Patch % Lines
...d/thv-operator/controllers/mcpserver_controller.go 7.95% 77 Missing and 4 partials ⚠️
...-operator/controllers/mcpremoteproxy_controller.go 6.09% 75 Missing and 2 partials ⚠️
cmd/thv-operator/pkg/controllerutil/authz.go 17.24% 70 Missing and 2 partials ⚠️
...perator/controllers/virtualmcpserver_controller.go 7.35% 60 Missing and 3 partials ⚠️
cmd/thv-operator/pkg/vmcpconfig/converter.go 0.00% 25 Missing and 2 partials ⚠️
...v-operator/controllers/mcpremoteproxy_runconfig.go 22.22% 5 Missing and 2 partials ⚠️
...md/thv-operator/controllers/mcpserver_runconfig.go 0.00% 5 Missing and 2 partials ⚠️
...v-operator/pkg/virtualmcpserverstatus/collector.go 16.66% 4 Missing and 1 partial ⚠️
Additional details and impacted files
@@                       Coverage Diff                        @@
##           jaosorior/mcpauthzconfig-crd    #4778      +/-   ##
================================================================
- Coverage                         68.85%   68.44%   -0.41%     
================================================================
  Files                               520      519       -1     
  Lines                             55137    55199      +62     
================================================================
- Hits                              37964    37783     -181     
- Misses                            14242    14499     +257     
+ Partials                           2931     2917      -14     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment on lines 2338 to +2356
)
}

// handleAuthzConfig validates and tracks the hash of the referenced MCPAuthzConfig.
// It updates the MCPServer status when the authorization configuration changes and sets
// the AuthzConfigRefValidated condition.
func (r *MCPServerReconciler) handleAuthzConfig(ctx context.Context, m *mcpv1alpha1.MCPServer) error {
ctxLogger := log.FromContext(ctx)

if m.Spec.AuthzConfigRef == nil {
// No MCPAuthzConfig referenced, clear any stored hash
if m.Status.AuthzConfigHash != "" {
m.Status.AuthzConfigHash = ""
if err := r.Status().Update(ctx, m); err != nil {
return fmt.Errorf("failed to clear MCPAuthzConfig hash from status: %w", err)
}
}
return nil
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 When AuthzConfigRef is removed from an MCPServer or VirtualMCPServer spec, the ConditionAuthzConfigRefValidated condition is left stale in status instead of being cleared. This causes the condition to remain True indefinitely, misleading operators and any tooling that inspects status conditions about whether authorization is still active.

Extended reasoning...

Root cause — MCPServer

In MCPServer.handleAuthzConfig() (mcpserver_controller.go), the nil-ref branch clears AuthzConfigHash but never calls meta.RemoveStatusCondition:

if m.Spec.AuthzConfigRef == nil {
    if m.Status.AuthzConfigHash != "" {
        m.Status.AuthzConfigHash = ""
        if err := r.Status().Update(ctx, m); err != nil { ... }
    }
    return nil   // ← ConditionAuthzConfigRefValidated is never removed
}

Root cause — VirtualMCPServer

In VirtualMCPServer.handleAuthzConfig() (virtualmcpserver_controller.go), the nil-ref branch has the same omission through the StatusManager abstraction:

if vmcp.Spec.IncomingAuth == nil || vmcp.Spec.IncomingAuth.AuthzConfigRef == nil {
    if vmcp.Status.AuthzConfigHash != "" {
        statusManager.SetAuthzConfigHash("")
    }
    return nil   // ← no SetCondition/removal call for ConditionAuthzConfigRefValidated
}

The reference implementation shows the correct pattern

MCPRemoteProxy.handleAuthzConfig() (introduced in the same PR) does it correctly:

if proxy.Spec.AuthzConfigRef == nil {
    meta.RemoveStatusCondition(&proxy.Status.Conditions, mcpv1alpha1.ConditionAuthzConfigRefValidated)
    if proxy.Status.AuthzConfigHash != "" { ... }
    return nil
}

Step-by-step proof (MCPServer)

  1. User creates an MCPServer with spec.authzConfigRef.name: my-authz.
  2. Controller reconciles: handleAuthzConfig fetches the config, validates it, sets ConditionAuthzConfigRefValidated=True and stores the hash.
  3. User removes spec.authzConfigRef from the MCPServer (sets to nil).
  4. Controller reconciles: handleAuthzConfig enters the nil branch, clears AuthzConfigHash, and returns — no call to meta.RemoveStatusCondition.
  5. Result: kubectl get mcpserver <name> -o yaml still shows ConditionAuthzConfigRefValidated=True indefinitely, even though no authorization config is referenced.

The same sequence applies to VirtualMCPServer via the StatusManager path.

Why existing code doesn't prevent it

The StatusCollector for VirtualMCPServer is instantiated fresh each reconciliation with an empty conditions map. Conditions not explicitly set during a reconcile pass are left untouched in the live status (only conditions stored in s.conditions are applied in UpdateStatus). So the omission of a SetCondition call with removal intent means the live stale condition persists.

Fix

  • MCPServer: Add meta.RemoveStatusCondition(&m.Status.Conditions, mcpv1alpha1.ConditionAuthzConfigRefValidated) at the start of the nil branch, matching MCPRemoteProxy.
  • VirtualMCPServer: Call statusManager.SetCondition(mcpv1alpha1.ConditionAuthzConfigRefValidated, "", "", "") or use the existing empty-Status marker trick in RemoveConditionsWithPrefix so the collector removes the condition in UpdateStatus.

jhrozek
jhrozek previously approved these changes Apr 13, 2026

@jhrozek jhrozek left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good -- solid pattern consistency with MCPOIDCConfig. Two minor nits inline.

// set the type. Full support for other backends is a follow-up.
return &vmcpconfig.AuthzConfig{
Type: authzConfig.Spec.Type,
}, nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: For non-Cedar types this returns an AuthzConfig with only Type set and no actual config. At runtime the vMCP would fail with an unclear error. Would it be better to fail fast here?

Suggested change
}, nil
return nil, fmt.Errorf("MCPAuthzConfig type %%q is not yet supported for VirtualMCPServer; only cedarv1 is supported", authzConfig.Spec.Type)

@jhrozek

jhrozek commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

nit: cmd/thv-operator/controllers/mcpauthzconfig_controller.go — The MCPOIDCConfig controller declares RBAC markers for the workload types it lists/watches (virtualmcpservers, mcpremoteproxies). This controller lists all three workload types in findReferencingWorkloads and watches them in SetupWithManager, but only declares RBAC for mcpauthzconfigs. Works today via aggregation from other controllers, but should these be added for completeness?

// +kubebuilder:rbac:groups=toolhive.stacklok.dev,resources=mcpservers,verbs=get;list;watch
// +kubebuilder:rbac:groups=toolhive.stacklok.dev,resources=virtualmcpservers,verbs=get;list;watch
// +kubebuilder:rbac:groups=toolhive.stacklok.dev,resources=mcpremoteproxies,verbs=get;list;watch

@JAORMX JAORMX force-pushed the jaosorior/mcpauthzconfig-crd branch from 734e8db to d571e89 Compare April 14, 2026 06:19
@JAORMX JAORMX force-pushed the jaosorior/mcpauthzconfig-crd branch from d571e89 to 321c747 Compare May 29, 2026 11:19
ChrisJBurns added a commit that referenced this pull request Jun 11, 2026
No out-of-package caller exists for this helper today. PR #4778 will
need it once workload controllers resolve AuthzConfigRef, and the
cleaner home at that point will be cmd/thv-operator/pkg/controllerutil/
alongside BuildInlineCedarAuthzConfig. Until then keep the signature
package-private so the contract can evolve without breaking external
consumers.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ChrisJBurns added a commit that referenced this pull request Jun 11, 2026
The staging note on AuthzConfigRef becomes inaccurate the moment the
workload-controller wiring lands. A grep-able TODO(#4778) marker
in the Go source ensures whoever lands #4778 finds the stale text
without manually scanning three type files. controller-gen strips
TODO lines from the rendered CRD description, so the user-facing
schema stays clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ChrisJBurns added a commit that referenced this pull request Jun 12, 2026
* Add MCPAuthzConfig CRD for backend-agnostic authz

Introduce a namespace-scoped MCPAuthzConfig CRD so authorization
policy can be defined once and shared across MCPServer,
MCPRemoteProxy, and VirtualMCPServer workloads, mirroring the
existing MCPOIDCConfig sharing pattern.

The spec carries a backend Type plus an opaque Config RawExtension.
A ConfigKey() method on AuthorizerFactory (cedar->"cedar",
http->"pdp") lets the controller reconstruct the full authorizer
config and validate it via the factory registry, keeping the
controller backend-agnostic; the backends are registered through
blank imports in the operator entrypoint.

The controller validates the spec, computes a config hash, manages a
finalizer, tracks referencing workloads, and blocks deletion while
referenced. AuthzConfigRef fields are added to the three workload
specs with CEL XValidation enforcing mutual exclusivity against the
existing inline authzConfig, matching the oidcConfig/oidcConfigRef
pattern.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Add mcpauthzconfigs to RBAC E2E golden fixtures

The chainsaw operator RBAC assertions compare the live
toolhive-operator-manager-role ClusterRole against a hardcoded
expected manifest. Adding the MCPAuthzConfig CRD introduced new
mcpauthzconfigs rules in the generated ClusterRole, making the
fixtures stale and failing E2E on all kind versions.

Add the mcpauthzconfigs, mcpauthzconfigs/finalizers, and
mcpauthzconfigs/status entries in the same alphabetical position the
generated role uses (after embeddingservers*, before
mcpexternalauthconfigs*) to both the multi-tenancy and single-tenancy
setup fixtures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Guard reserved ConfigKey values and tighten MCPAuthzConfig.Validate

Register() now panics if a factory's ConfigKey() returns one of the
reserved envelope keys ("", "version", "type"). BuildFullAuthzConfigJSON
assembles its config with a Go map literal where those two keys are
fixed metadata; a factory returning a reserved key would silently
overwrite the envelope. Catching this at startup makes the
misconfiguration impossible to ship.

MCPAuthzConfig.Validate() now consults authorizers.IsRegistered so any
caller (CLI tooling, future webhook, defense-in-depth fallback) fails
closed on an unknown spec.type rather than deferring entirely to the
controller. The unit tests blank-import the cedar backend so the
existing "cedarv1" cases continue to pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Canonicalize MCPAuthzConfig spec before hashing

The config hash is computed via ctrlutil.CalculateConfigHash, which walks
the spec struct including Spec.Config.Raw. RawExtension preserves the
user's raw bytes verbatim, so two semantically-equal configs that differ
only in whitespace or JSON key order produced different hashes and flipped
status on every noop kubectl-apply round-trip.

canonicalizeSpecForHash returns a copy of the spec whose Config.Raw has
been re-marshalled (Go's encoding/json sorts map keys), giving a stable
hash for the same logical config regardless of source formatting.
Malformed JSON is returned unchanged so Validate / validateAuthzConfig
remain the source of the user-facing error.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Use MutateAndPatchStatus/Spec in MCPAuthzConfig controller

Status writes now flow through controllerutil.MutateAndPatchStatus and
finalizer writes through controllerutil.MutateAndPatchSpec, per the
operator rule (.claude/rules/operator.md). The previous r.Status().Update
calls sent full PUT bodies that would clobber conditions written by any
disjoint owner of Status.Conditions on this CRD; the previous r.Update
calls had no optimistic-lock guard around finalizer arrays.

Condition and field mutations have moved inside the helper closures so
the pre-mutate snapshot reflects the live state rather than already
containing the change — a MutateAndPatchStatus prerequisite. A small
helper, setValidTrueCondition, factors out the Valid=True transition so
the success path doesn't duplicate the metav1.Condition literal.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Tighten MCPAuthzConfig reconcile flow

Collapses the previously-separate hash-change branch and steady-state
status update into a single MutateAndPatchStatus call on the success
path. Side effects of doing so:

- F4: ReferencingWorkloads / ReferenceCount now refresh in the same
  reconcile that writes a new ConfigHash, so the print column doesn't
  lag a workload event behind.
- F8: The success path explicitly removes the DeletionBlocked
  condition. A user who cancels a deletion (e.g., by removing the
  finalizer manually after observing the block) no longer carries a
  stale DeletionBlocked=True forward across reconciles.
- F9: findReferencingWorkloads errors now return up to controller-
  runtime instead of being logged and swallowed, so a transient
  apiserver hiccup is retried with backoff rather than silently
  writing an empty references list.

Two new tests exercise the F4 (one-reconcile property) and F8
(condition-clear) paths.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Unexport buildFullAuthzConfigJSON

No out-of-package caller exists for this helper today. PR #4778 will
need it once workload controllers resolve AuthzConfigRef, and the
cleaner home at that point will be cmd/thv-operator/pkg/controllerutil/
alongside BuildInlineCedarAuthzConfig. Until then keep the signature
package-private so the contract can evolve without breaking external
consumers.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Declare mcpservers RBAC marker on MCPAuthzConfig controller

findReferencingWorkloads lists MCPServer alongside VirtualMCPServer and
MCPRemoteProxy, but the kubebuilder marker only declared the latter
two. The clusterrole currently works because mcpserver_controller's
markers cover get;list;watch on mcpservers, so the rendered output is
unchanged here — but a future regeneration that reshuffles those
declarations could silently strip the permission the MCPAuthzConfig
controller actually depends on.

Verified the rendered clusterrole and chainsaw asserts are unchanged
after task operator-manifests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Test condition preservation and last-ref finalizer transition

Two new tests for MCPAuthzConfig reconciler coverage:

- FinalizerRemovedAfterLastRefDropped: exercises the N→0 transition
  in handleDeletion. The existing static-state cases cover "blocked"
  and "no refs" endpoints but not the flip between them, which is the
  user-observed behaviour for policy rotation.

- PreservesForeignConditions: the canary for the merge-patch
  conditions-array semantic. JSON merge-patch on CRDs replaces the
  conditions array wholesale (the +listType=map marker only matters to
  strategic-merge-patch), so a regression that re-introduces
  r.Status().Update on this resource — or that pre-mutates Status
  outside a MutateAndPatchStatus closure — would erase any concurrently-
  set foreign condition. This test fails in that scenario.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Add envtest for authzConfig/authzConfigRef mutual exclusion

CEL XValidation rules run at API admission, which unit tests with the
fake client cannot exercise. The three workload specs (MCPServer,
MCPRemoteProxy, VirtualMCPServer.IncomingAuth) each declare the same
mutex rule between the legacy inline authzConfig and the new
authzConfigRef — without envtest coverage the rules could silently
regress on a CRD regeneration.

For each spec, the new tests apply only-inline, only-ref, and both-set
CRs and assert (c) is rejected with the expected message. They follow
the existing pattern in cmd/thv-operator/test-integration/.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Reframe AuthzConfigRef as part 1 of 2 in godocs

The new AuthzConfigRef field on MCPServer, MCPRemoteProxy, and
VirtualMCPServer.IncomingAuth is wired into the MCPAuthzConfig
controller's reference tracking (status.referenceCount, deletion
protection) but no workload controller actually resolves the ref into a
runtime authz config in this PR. That wiring lands in a follow-up.

The previous godocs marked the inline AuthzConfig field "Deprecated:
Use AuthzConfigRef" — which pointed users at a non-functional field
and risked workloads running with no authorization enforced. Drop the
premature Deprecated annotation and add an explicit NOTE on
AuthzConfigRef so adopters know to stick with the inline form until
the consumer-side wiring lands.

The CEL mutex rule and the controller's reference tracking are
unchanged; only the descriptive godocs move.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Regenerate CRD manifests and reference docs

Picks up the F1 godoc edits: the CRD YAML descriptions and the
generated CRD API reference (docs/operator/crd-api.md) now match the
authoritative godoc on AuthzConfigRef / AuthzConfig (no premature
Deprecated annotation; explicit "consumed in a follow-up PR" note on
AuthzConfigRef).

Generated by task operator-generate + task operator-manifests +
task crdref-gen.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Use compound (kind, name) listMapKey on ReferencingWorkloads

The original review of this PR flagged that +listMapKey=name alone is
insufficient: two workloads of different kinds that share a name (an
MCPServer "foo" and a VirtualMCPServer "foo") would collide under
merge-patch semantics, with the second-applied entry silently
overwriting the first. Adding +listMapKey=kind makes the map key the
(kind, name) pair so cross-kind name reuse stays distinct.

Limited to MCPAuthzConfig here. The two sibling controllers
(MCPOIDCConfig, MCPExternalAuthConfig) share the same WorkloadReference
type and have the same listMapKey=name asymmetry — filed as a parity
follow-up rather than expanding this PR's scope.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Refresh validateAuthzConfig doc comment

The prior comment said backend validation lived in the controller "because
the API types package must not import the authorizer backends" — but the
types package now imports pkg/authz/authorizers to call IsRegistered in
Validate(). Restate the real reason: type-level Validate handles structural
+ registration checks; backend ValidateConfig requires the full
reconstructed JSON envelope that only the controller builds.

Drive-by: gitignore .pr-review/ so PR-review skill scratch dirs don't
pollute git status in worktrees.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Thread factory through buildFullAuthzConfigJSON, drop redundant lookups

buildFullAuthzConfigJSON now returns the resolved AuthorizerFactory
alongside the envelope JSON, so validateAuthzConfig can dispatch
ValidateConfig directly without a second GetFactory call and without
re-Unmarshalling the JSON it just built.

Before: Validate() ran IsRegistered, buildFullAuthzConfigJSON ran
GetFactory, validateAuthzConfig re-Unmarshalled the envelope and ran
GetFactory again — three near-identical lookups for the same key on
every reconcile, with three slightly different error messages.

The authzConfigEnvelope struct is removed; nothing else consumed it.
Tests updated to assert the returned factory's ConfigKey matches the
expected nested envelope key, locking in the bidirectional contract.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Drop marshalJSONString in favor of strconv.Quote

json.Marshal of a Go string cannot fail at runtime — encoding/json's
stringEncoder has no error path for valid Go strings. The helper existed
to placate errcheck rather than to catch a real failure mode, adding two
unreachable error branches per call site. strconv.Quote produces the
same JSON-encoded bytes with no error-checking overhead, and the call
sites become readable single expressions.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Demote hash-change log to DEBUG

Silent-success per .claude/rules/go-style.md: routine state transitions
log at DEBUG, INFO is reserved for long-running operations. A noisy
INFO on every kubectl-apply that bumps a hash is exactly the case the
rule was written to prevent.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Mark AuthzConfigRef staging NOTE with TODO(#4778)

The staging note on AuthzConfigRef becomes inaccurate the moment the
workload-controller wiring lands. A grep-able TODO(#4778) marker
in the Go source ensures whoever lands #4778 finds the stale text
without manually scanning three type files. controller-gen strips
TODO lines from the rendered CRD description, so the user-facing
schema stays clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Lock ConfigKey()/struct tag contract per backend

Factory.ConfigKey() returns a string ("cedar" / "pdp") that must match
the json tag on the backend's Config.Options field — otherwise the
controller will emit envelope JSON the backend's own Unmarshal cannot
parse. A new TestFactory_ConfigKeyMatchesStructTag per backend builds
an envelope around ConfigKey() and asserts it deserialises with
Options populated. A future rename of either string in isolation will
fail the test.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Tighten test docstrings, fixtures, and assertions

PreservesForeignConditions: docstring reframed to describe the property
it actually guards (snapshot-and-diff doesn't erase foreign conditions
during patch construction) and to be explicit about what it does NOT
catch (r.Status().Update under the fake client — requires a
WithInterceptorFuncs-backed concurrent-writer scenario).

HashAndRefsLandInOneReconcile: the MCPServer fixture now sets the
required Image field so the test remains portable to envtest. Added an
ObservedGeneration assertion so all four fields written in the single
MutateAndPatchStatus closure are pinned (previously hash, references,
referencingWorkloads.name were locked but ObservedGeneration was not).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Update cmd/thv-operator/controllers/mcpauthzconfig_controller.go

Co-authored-by: Jakub Hrozek <jakub.hrozek@posteo.se>

* Remove unused strconv import

Cascade fix from the web-UI edit that replaced strconv.Quote with
json.Marshal: the import was left in place, breaking compilation and
every downstream CI job (lint, tests, govulncheck x2, helm lint, e2e
lifecycle x3).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Chris Burns <29541485+ChrisJBurns@users.noreply.github.com>
Co-authored-by: Jakub Hrozek <jakub.hrozek@posteo.se>
Base automatically changed from jaosorior/mcpauthzconfig-crd to main June 12, 2026 12:29
@ChrisJBurns ChrisJBurns dismissed jhrozek’s stale review June 12, 2026 12:29

The base branch was changed.

ChrisJBurns added a commit that referenced this pull request Jun 18, 2026
* Add MCPAuthzConfig ref-resolution foundation

Foundation (Stage 1 of #4778's re-derivation) for making spec.authzConfigRef
enforce at runtime. Purely additive: no workload controller is wired yet and
the inline spec.authzConfig path is untouched.

- Add controllerutil ref-helpers in authz_ref.go: GetAuthzConfigForWorkload,
  ValidateAuthzConfigReady, AddAuthzConfigRefOptions (resolves a referenced
  MCPAuthzConfig into a runner authz.Config for any registered backend —
  cedarv1 and httpv1 — via the authorizers factory), and the
  EnsureAuthzConfigMapFromRef / GenerateAuthzVolumeConfigFromRef materialization
  helpers mirroring the inline ones.
- Export and move BuildFullAuthzConfigJSON from the MCPAuthzConfig controller
  into controllerutil so both the config controller and the workload controllers
  share it without an import cycle; the controller now calls the shared helper.
- Add the AuthzConfigHash status field to MCPServer, MCPRemoteProxy, and
  VirtualMCPServer, plus ConditionAuthzConfigRefValidated and reason constants,
  mirroring the OIDCConfigRef equivalents.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Regenerate CRD manifests and docs for AuthzConfigHash

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Address review feedback on authz ref helpers

- Guard EnsureAuthzConfigMapFromRef with ValidateAuthzConfigReady so the
  exported helper never materializes a ConfigMap from a config flagged invalid
  (defense in depth; no extra I/O — the object is already fetched). (F2)
- Clarify the authzRefConfigMapName doc comment: the ConfigMap *name* is
  distinct from the inline path, while the *volume* name and mount path are
  deliberately shared (inline and ref are mutually exclusive via CRD
  XValidation, so a workload mounts at most one authz volume). (F3)
- Restore the nil data/factory assertions on BuildFullAuthzConfigJSON's error
  branch. (F5)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ChrisJBurns

Copy link
Copy Markdown
Collaborator

Closing as its being superseeded

ChrisJBurns added a commit that referenced this pull request Jun 19, 2026
The AuthzConfigRef field docs on MCPServer and MCPRemoteProxy still
carried the TODO(#4778) + NOTE saying the ref is reference-tracked but
does NOT apply authorization. Both controllers now resolve the ref into
runtime authz, so the note states the opposite of the behavior and would
mislead anyone reading the generated CRD reference. Remove it and
regenerate manifests + API docs.

Addresses review feedback on #5563.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/L Large PR: 600-999 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants